/*
 * Copyright (C) 2002-2005 the xine project
 *
 * This file is part of xine, a free video player.
 *
 * xine is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * xine is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * $Id: http.c,v 1.28 2006/04/08 14:44:44 dsalt Exp $
 */

#include "globals.h"
#include "defs.h"

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>

#include <netinet/in.h>

#include "http.h"
#include "utils.h"

/*
#define LOG
*/

#define BUFSIZE                 1024
#define BUFSIZE_STR		"1023"
#define DEFAULT_HTTP_PORT         80

typedef struct http_s http_t;

struct http_s {

  int              fh;

  off_t            curpos;
  off_t            contentlength;

  char             mime_type[BUFSIZE];

  char             buf[BUFSIZE];
  char             mrlbuf[BUFSIZE];
  char             proxybuf[BUFSIZE];

  char             auth[BUFSIZE];
  char             proxyauth[BUFSIZE];

  char            *user;
  char            *password;
  char            *host;
  int              port;
  char            *filename;

  char            *proxyuser;
  char            *proxypassword;
  char            *proxyhost;
  int              proxyport;

  int		   redir_count;
};

static int http_host_connect_attempt (struct in_addr ia, int port)
{
  union {
    struct sockaddr_in i;
    struct sockaddr s;
  } sin;

  int s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (s == -1)
  {
    display_error (FROM_GXINE, _("HTTP error"),
		   _("Failed to open socket: %s"), strerror (errno));
    return -1;
  }

  sin.i.sin_family = AF_INET;
  sin.i.sin_addr   = ia;
  sin.i.sin_port   = htons(port);

  if (connect (s, &sin.s, sizeof (sin))==-1
      && errno != EINPROGRESS)
  {
    display_error (FROM_GXINE, _("HTTP error"),
		   _("Cannot connect to host: %s"), strerror (errno));
    close (s);
    return -1;
  }

  return s;
}

static int http_host_connect (const char *host, int port)
{
  struct hostent *h = gethostbyname(host);
  if (h==NULL)
  {
    display_error (FROM_GXINE, _("HTTP error"),
		   _("Unable to resolve ‘%s’"), host);
    return -1;
  }

  int i, s;
  for (i = 0, s = -1; s == -1 && h->h_addr_list[i]; ++i)
  {
    struct in_addr ia;
    memcpy (&ia, h->h_addr_list[i], 4);
    s = http_host_connect_attempt (ia, port);
  }

  return s;
}

static int http_parse_url (char *urlbuf, char **user, char **password,
			   char** host, int *port, char **filename)
{
  char *start, *at, *portcolon, *slash;
  char *authcolon = NULL;

  if (user != NULL)
    *user = NULL;

  if (password != NULL)
    *password = NULL;

  if (host != NULL)
    *host = NULL;

  if (filename != NULL)
    *filename = NULL;

  if (port != NULL)
    *port = 0;

  start = strstr(urlbuf, "://");
  if (start != NULL)
    start += 3;
  else
    start = urlbuf;

  at = strchr(start, '@');
  slash = strchr(start, '/');

  if (at != NULL && slash != NULL && at > slash)
    at = NULL;

  if (at != NULL)
  {
    authcolon = strchr(start, ':');
    if(authcolon != NULL && authcolon > at)
      authcolon = NULL;

    portcolon = strchr(at, ':');
  }
  else
    portcolon = strchr(start, ':');

  if (portcolon != NULL && slash != NULL && portcolon > slash)
    portcolon = NULL;

  if (at != NULL)
  {
    *at = '\0';

    if (user != NULL)
      *user = start;

    if (authcolon != NULL)
    {
      *authcolon = '\0';

      if (password != NULL)
      	*password = authcolon + 1;
    }

    if (host != NULL)
      *host = at + 1;
  } else
    if (host != NULL)
      *host = start;

  if (slash != NULL)
  {
    *slash = '\0';

    if (filename != NULL)
      *filename = slash + 1;
  } else if (filename != NULL)
    *filename = urlbuf + strlen(urlbuf);

  if (portcolon != NULL)
  {
    *portcolon = '\0';

    if (port != NULL)
      *port = atoi(portcolon + 1);
  }

  return 0;
}

static int http_basicauth (const char *user, const char *password,
			   char* dest, int len)
{
  static const char enctable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  char        *tmp;
  char        *sptr;
  char        *dptr;
  int          count;

  count = strlen (user) + 1;
  if (password != NULL)
    count += strlen (password);

  if (len < ((count + 2) / 3) * 4 + 1)
    return -1;

  sptr = tmp = g_strconcat (user, ":", password, NULL);
  dptr = dest;
  while (count >= 3)
  {
    *dptr++ = enctable[(sptr[0] & 0xFC) >> 2];
    *dptr++ = enctable[((sptr[0] & 0x3) << 4) | ((sptr[1] & 0xF0) >> 4)];
    *dptr++ = enctable[((sptr[1] & 0x0F) << 2) | ((sptr[2] & 0xC0) >> 6)];
    *dptr++ = enctable[sptr[2] & 0x3F];
    count -= 3;
    sptr += 3;
  }

  if (count > 0)
  {
    *dptr++ = enctable[(sptr[0] & 0xFC) >> 2];

    if (count > 1)
    {
      *dptr++ = enctable[((sptr[0] & 0x3) << 4) | ((sptr[1] & 0xF0) >> 4)];
      *dptr++ = enctable[(sptr[1] & 0x0F) << 2];
    }
    else
    {
      *dptr++ = enctable[(sptr[0] & 0x3) << 4];
      *dptr++ = '=';
    }

    *dptr++ = '=';
  }

  dptr[0] = '\0';

  free (tmp);
  return 0;
}

static void __attribute__ ((format (printf, 2, 3)))
report_error (const char *url, const char *fmt, ...)
{
  va_list ap;
  char *msg;
  va_start (ap, fmt);
  msg = g_strdup_vprintf (fmt, ap);
  va_end (ap);
  display_error (FROM_GXINE, _("HTTP error"),
		 _("There was an error while trying to download\n%s\n:\n%s"),
		 url, msg);
  free (msg);
}

static http_t *http_open (const char *mrl)
{
  char     *proxy, *request;
  int       done, len, linenum;
  char      mime_type[BUFSIZE];
  char      location[BUFSIZE];
  int       redirect = 0;

  http_t *this = malloc (sizeof (http_t));

  this->redir_count = 0;
  snprintf (this->mrlbuf, sizeof (this->mrlbuf), "%s", mrl);

  this->proxybuf[0] = '\0';
  proxy = getenv("http_proxy");

  retry:

  strcpy (location, this->mrlbuf);

  if (proxy != NULL)
  {
    snprintf (this->proxybuf, sizeof (this->proxybuf), "%s", proxy);

    if (http_parse_url (this->proxybuf, &this->proxyuser,
			&this->proxypassword, &this->proxyhost,
			&this->proxyport, NULL))
    {
      /* malformed URL */
      report_error (location, _("URL parse error"));
      free (this);
      return NULL;
    }

    if (this->proxyport == 0)
      this->proxyport = DEFAULT_HTTP_PORT;

    if (this->proxyuser != NULL &&
        http_basicauth (this->proxyuser, this->proxypassword,
			this->proxyauth, BUFSIZE))
    {
      report_error (location, _("proxy authentication error"));
      free (this);
      return NULL;
    }
  }

  if (http_parse_url (this->mrlbuf, &this->user, &this->password,
		      &this->host, &this->port, &this->filename))
  {
    free (this);
    /* malformed URL */
    report_error (location, _("URL parse error"));
    return NULL;
  }

  if (this->port == 0)
    this->port = DEFAULT_HTTP_PORT;

  if (this->user != NULL &&
      http_basicauth (this->user, this->password, this->auth, BUFSIZE))
  {
    report_error (location, _("proxy authentication error"));
    free (this);
    return NULL;
  }

  if (proxy != NULL)
    logprintf ("http: opening /%s on host %s via proxy %s",
	       this->filename, this->host, this->proxyhost);
  else
    logprintf ("http: opening /%s on host %s",
	       this->filename, this->host);

  if (proxy != NULL)
    this->fh = http_host_connect (this->proxyhost, this->proxyport);
  else
    this->fh = http_host_connect (this->host, this->port);

  this->curpos = 0;

  if (this->fh == -1)
  { /* couldn't connect */
    report_error (location, _("host connect error"));
    free (this);
    return NULL;
  }

  if (proxy != NULL)
  {
    if (this->port != DEFAULT_HTTP_PORT)
      request = g_strdup_printf ("GET http://%s:%d/%s HTTP/1.0\015\012",
		       this->host, this->port, this->filename);
    else
      request = g_strdup_printf ("GET http://%s/%s HTTP/1.0\015\012",
		       this->host, this->filename);
  }
  else
    request = g_strdup_printf ("GET /%s HTTP/1.0\015\012", this->filename);

  if (this->port != DEFAULT_HTTP_PORT)
    asreprintf (&request, "%sHost: %s:%d\015\012", request,
		this->host, this->port);
  else
    asreprintf (&request, "%sHost: %s\015\012", request, this->host);

  if (this->proxyuser != NULL)
    asreprintf (&request, "%sProxy-Authorization: Basic %s\015\012", request,
		this->proxyauth);

  if (this->user != NULL)
    asreprintf (&request, "%sAuthorization: Basic %s\015\012", request,
	     this->auth);

  asreprintf (&request,
	      "%sUser-Agent: xine/%s\015\012Accept: */*\015\012\015\012",
	      request, VERSION);

  len = strlen (request);
  if (write (this->fh, request, len) != len)
  {
    free (request);
    report_error (location, _("couldn't send request"));
abort:
    close (this->fh);
    free (this);
    return NULL;
  }

  logprintf ("http: request sent: >%s<\n", request);
  free (request);

  /* read and parse reply */
  done = len = linenum = 0;
  this->contentlength = 0;

  while (!done)
  {
    if (read (this->fh, &this->buf[len], 1) <=0)
    {
      switch (errno)
      {
      case EAGAIN:
	logprintf ("http: EAGAIN\n");
	continue;
      default:
	report_error (location, _("read error: %s"), strerror (errno));
	goto abort;
      }
    }

    if (this->buf[len] == '\012' || len >= (BUFSIZE - 2))
    {
      this->buf[len] = '\0';
      len--;

      if (len >= 0 && this->buf[len] == '\015')
      {
	this->buf[len] = '\0';
	len--;
      }

      linenum++;

      logprintf ("input_http: answer: >%s<\n", this->buf);

      if (linenum == 1)
      {
        int httpver, httpsub, httpcode;
	char httpstatus[BUFSIZE];

	if (sscanf(this->buf, "HTTP/%d.%d %d %[^\015\012]", &httpver, &httpsub,
		   &httpcode, httpstatus) != 4)
	{
	  report_error (location, _("invalid HTTP response"));
	  goto abort;
	}

	switch (httpcode)
	{
	/* The following codes have cacheable Location headers,
	 * but we don't yet handle updating the MRLs.
	 */
	case 300: /* Multiple Choices */
	case 301: /* Moved Permanently */
	/* The following codes have uncacheable Location headers. */
	case 302: /* Found */
	case 303: /* See Other */
	case 307: /* Temporary Redirect */
	  if (++this->redir_count >= 10)
	  {
	    report_error (mrl, _("Too many HTTP redirections; aborted"));
	    goto abort;
	  }
	  redirect = httpcode;
	  break;

	case 200 ... 299:
	  break;

	default:
	  report_error (location, _("Unexpected HTTP status (%d)"), httpcode);
	    goto abort;
	}
      }
      else
      {
	if (this->contentlength == 0)
	{
	  off_t contentlength;
	  if (sscanf(this->buf, "Content-Length: %ld", &contentlength) == 1)
	  {
	    logprintf ("http: content length = %ld bytes\n", contentlength);
	    this->contentlength = contentlength;
	  }
        }
        if (sscanf(this->buf, "Content-Type: %"BUFSIZE_STR"s", mime_type) == 1)
        {
	  logprintf ("http: content type = '%s'\n", mime_type);
	  strcpy (this->mime_type, mime_type); /* note buffer sizes */
	}
	if (redirect && !strncasecmp(this->buf, "Location:", 9))
	{
	  off_t index = 9;
	  while (isspace (this->buf[index]))
	    ++index;
	  snprintf (this->mrlbuf, sizeof (this->mrlbuf), "%s", this->buf + 10);
	  logprintf ("http: location = '%s'\n", this->mrlbuf);
	  close (this->fh); /* heading for disconnect anyway due to protocol */
	  redirect = 0;
	  goto retry;
	}
      }

      if (len == -1)
	done = 1;
      else
	len = 0;
    } else
      len ++;
  }

  if (redirect)
  { /* the server says that the resource is elsewhere but hasn't said where */
    report_error (location, _("HTTP redirection (%d) is missing a location"), redirect);
    goto abort;
  }

  return this;
}



char *http_download (const char *url, ssize_t *file_size)
{
  logprintf ("http: attempt to download %s\n", url);

  if (file_size)
    *file_size = 0;

  http_t *http = http_open (url);
  if (!http)
    return NULL;

  logprintf ("http: connected\n");

  char   *buf = NULL;
  ssize_t total = 0;
  for (;;)
  {
    int n;

    buf = realloc (buf, total + 4098); /* two extra bytes for termination */
    n = read (http->fh, &buf[total], 4096);

    if (n > 0)		/* got some data */
      total += n;
    else if (n == 0)	/* end of stream */
      break;
    else		/* error */
    {
      switch (errno)
      {
      case EAGAIN:
	logprintf ("http: EAGAIN\n");
	continue;
      default:
        /* FIXME: report errno in a window? */
	g_print (_("http: read error: %s\n"), strerror (errno));
	free(buf);
	return NULL;
      }
    }
  }

  logprintf ("http: got %zd bytes of content\n%s", total, buf);

  close (http->fh);

  free (http);

  if (file_size)
    *file_size = total;

  /* ensure that the buffer is LF+NUL-terminated
   * (don't include this in the buffer size!)
   */
  if (total && buf[total - 1] != '\n')
    buf[total++] = '\n';
  buf[total] = 0;

  return buf;
}

ssize_t http_peek (const char *url, char *buffer, size_t buffer_size,
	       char *mime_type, size_t mime_type_size)
{
  logprintf ("http: attempt to download %zu bytes from %s\n", buffer_size, url);

  http_t *http = http_open (url);
  if (!http)
    return 0;

  if (mime_type && mime_type_size)
    snprintf (mime_type, mime_type_size, "%s", http->mime_type);

  logprintf ("http: connected\n");

  int total = read (http->fh, buffer, buffer_size);

  close (http->fh);
  free (http);

  return total;
}
